﻿using System;
using System.Collections;
using System.Collections.Generic;

namespace NetAssist.Collections
{
  /// <summary>
  /// Represents a first-in, first-out buffer of objects. 
  /// Buffer is similar to queue but has a size limit.
  /// </summary>
  /// <typeparam name="T">Specifies the type of elements in the buffer.</typeparam>
  public class Buffer<T> : IEnumerable<T>, ICollection, IEnumerable
  {
    private int maxSize;
    private Queue<T> queue;

    /// <summary>
    /// Occurs after a push operation when the buffer is full.
    /// </summary>
    public event EventHandler<BufferOverflowEventArgs<T>> BufferOverflow;

    /// <summary>
    /// Initializes a new instance of the Buffer&lt;T&gt; class that is empty and has the initial size (capacity).
    /// </summary>    
    /// <param name="maxSize">Maximum number of objects to buffer.</param>
    /// <exception cref="System.ArgumentOutOfRangeException">maxSize must be 1 or larger.</exception>
    public Buffer(int maxSize)
    {
      Helper.InvalidateMinValue(maxSize, 1, "maxSize");
      this.maxSize = maxSize;
      this.Reset();
    }

    /// <summary>
    /// Reset the Buffer&lt;T&gt;, removing all its elements.
    /// </summary>
    public void Reset()
    {
      this.queue = new Queue<T>(this.maxSize);
    }

    /// <summary>
    /// Sets the Buffer&lt;T&gt; buffer capacity to new size.
    /// </summary>
    /// <param name="newSize">The new buffer size</param>
    /// <exception cref="System.ArgumentOutOfRangeException">newSize must be equal or larger than the count of existing buffer elements.</exception>
    public void Resize(int newSize)
    {
      Helper.InvalidateMinValue(newSize, this.Count, "minSize");
      this.maxSize = newSize;
    }

    /// <summary>
    /// Returns a value indicating if the Buffer&lt;T&gt; is full.
    /// </summary>
    /// <returns>True is the Buffer&lt;T&gt; is full (elements count is same as max size); otherwise, true.</returns>
    public bool Full
    {
      get { return this.Count == this.maxSize; }
    }

    /// <summary>
    /// Returns a value indicating if the Buffer&lt;T&gt; is empty.
    /// </summary>
    /// <returns>True is the Buffer&lt;T&gt; is empty (elements count is 0); otherwise, false.</returns>
    public bool Empty
    {
      get { return this.Count == 0; }
    }

    /// <summary>
    /// Returns a value indicating if the usage level of the Buffer&lt;T&gt;. This value is the ratio between current item count and max capacity.
    /// </summary>
    /// <returns>The ratio between current item count and max capacity.</returns>
    public float Usage
    {
      get
      {
        return (float)this.Count / this.maxSize;
      }
    }

    /// <summary>
    /// Gets the max size (capacity) of the Buffer&lt;T&gt;.
    /// </summary>
    public int MaxSize
    {
      get { return this.maxSize; }
    }


    /// <summary>
    /// Determines whether an element is in the Buffer&lt;T&gt;.
    /// </summary>
    /// <param name="item">The object to locate in the Buffer&lt;T&gt;. The value can be null for reference types.</param>
    /// <returns>true if item is found in the Buffer&lt;T&gt;; otherwise,false.</returns>
    public bool Contains(T item)
    {
      return this.queue.Contains(item);
    }

    /// <summary>
    /// Copies the Buffer&lt;T&gt; elements to an existing one-dimensional System.Array, starting at the specified array index.
    /// </summary>
    /// <param name="array">The one-dimensional System.Array that is the destination of the elements copied from Buffer&lt;T&gt;. The System.Array must have zero-based indexing.</param>
    /// <param name="index">The zero-based index in array at which copying begins.</param>
    /// <exception cref="System.ArgumentNullException">array is null.</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">arrayIndex is less than zero.</exception>    
    /// <exception cref="System.ArgumentException">The number of elements in the source Buffer&lt;T&gt; is greater than the available space from arrayIndex to the end of the destination array.</exception>    
    public void CopyTo(T[] array, int index)
    {
      this.queue.CopyTo(array, index);
    }

    /// <summary>
    /// Inserts an object at the beginning of the Buffer&lt;T&gt;.
    /// </summary>
    /// <param name="item">The object to push onto the Buffer&lt;T&gt;. The value can be null for reference types.</param>
    /// <remarks>If Buffer&lt;T&gt; is full, the item will be inserted at the beginning of the Buffer&lt;T&gt; and the item at the end will be popped automatically, The BufferOverflow event will be invoked as a result. The BufferOverflow event argument will include the popped item.</remarks>
    public void Push(T item)
    {
      if (this.Full)
      {
        T overflowItem = this.queue.Dequeue();
        this.queue.Enqueue(item);
        OnBufferOverflow(overflowItem);
      }
      else
        this.queue.Enqueue(item);
    }

    /// <summary>
    /// Removes and returns the object at the end of the Buffer&lt;T&gt;.
    /// </summary>
    /// <returns>The object removed from the end of the Buffer&lt;T&gt;.</returns>
    /// <exception cref="System.InvalidOperationException">The Buffer&lt;T&gt; is empty.</exception>
    public T Pop()
    {
      return this.queue.Dequeue();
    }

    /// <summary>
    /// Returns the object at the beginning of the Buffer&lt;T&gt; without removing it.
    /// </summary>
    /// <returns>The object at the beginning of the Buffer&lt;T&gt;.</returns>
    /// <exception cref="System.InvalidOperationException">The Buffer&lt;T&gt; is empty.</exception>    
    public T Peek()
    {
      return this.queue.Peek();
    }

    /// <summary>
    /// Returns the object at the zero-based location of the Buffer&lt;T&gt; without altering the buffer.
    /// </summary>
    /// <param name="index">Zero-based index of the item from the beginning of the buffer.</param>
    /// <returns>The object at the the position specified index.</returns>
    /// <exception cref="System.IndexOutOfRangeException">The index is out of the buffer items range.</exception>    
    public T this[int index]
    {
      get
      {
        if (index >= this.Count || index < 0)
          throw new IndexOutOfRangeException();

        return this.ToArray()[index];
      }
    }

    /// <summary>
    /// Copies the Buffer&lt;T&gt; elements to a new array.
    /// </summary>
    /// <returns>A new array containing elements copied from the Buffer&lt;T&gt;.</returns>
    public T[] ToArray()
    {
      return this.queue.ToArray();
    }

    #region IEnumerable<T> Members

    /// <summary>
    /// Returns an enumerator that iterates through the Buffer&lt;T&gt;.
    /// </summary>
    /// <returns>An Buffer&lt;T&gt;.Enumerator for the Buffer&lt;T&gt;.</returns>
    public IEnumerator<T> GetEnumerator()
    {
      return this.queue.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
      return this.GetEnumerator();
    }

    #endregion

    #region ICollection Members

    /// <summary>
    /// Copies the elements of the Buffer&lt;T&gt; to an System.Array, starting at a particular System.Array index.
    /// </summary>
    /// <param name="array">The one-dimensional System.Array that is the destination of the elements copied from Buffer&lt;T&gt;. The System.Array must have zero-based indexing.</param>
    /// <param name="index">The zero-based index in array at which copying begins.</param>
    /// <exception cref="System.ArgumentNullException">array is null.</exception>
    /// <exception cref="System.ArgumentOutOfRangeException">index is less than zero.</exception>
    /// <exception cref="System.ArgumentException">array is multidimensional.-or- The number of elements in the source Buffer&lt;T&gt; is greater 
    /// than the available space from index to the end of the destination array.-or-The type of the source Buffer&lt;T&gt; cannot be cast automatically to the type of the destination array.
    /// </exception>
    public void CopyTo(Array array, int index)
    {
      Array.Copy(this.ToArray(), 0, array, index, this.Count);
    }

    /// <summary>
    /// Gets the number of elements contained in the Buffer&lt;T&gt;.
    /// </summary>
    /// <returns>The number of elements contained in the Buffer&lt;T&gt;.</returns>
    public int Count
    {
      get { return this.queue.Count; }
    }

    /// <summary>
    /// Gets a value indicating whether access to the Buffer&lt;T&gt; is synchronized (thread safe).
    /// </summary>
    /// <returns>true if access to the Buffer&lt;T&gt; is synchronized (thread safe); otherwise, false.</returns>
    public bool IsSynchronized
    {
      get { return false; }
    }

    /// <summary>
    /// Gets an object that can be used to synchronize access to the Buffer&lt;T&gt;.
    /// </summary>
    /// <returns>An object that can be used to synchronize access to the Buffer&lt;T&gt;.</returns>
    public object SyncRoot
    {
      get { return this; }
    }

    #endregion

    /// <summary>
    /// invoked when a Push operation causes an overflow. 
    /// </summary>
    /// <param name="item">the item that got popped during the overflow.</param>
    protected void OnBufferOverflow(T item)
    {
      if (BufferOverflow != null)
        BufferOverflow(this, new BufferOverflowEventArgs<T>(item));
    }
  }
}